/******************************************************************************
 *  _____     _____
 *  \  __\    \  __\
 *   \ \_\\    \ \_\\
 *    \  __\SS  \  __\roject
 *     \_\       \_\
 *
 * Mateusz Maciąg, Rafał Urbański
 * Komputerowe Systemy Sterowania
 *****************************************************************************/

#include "ConfigReader.h"

#include "Utils.h"

#include <cctype>
#include <string>
#include <sstream>

#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>

// Przestrzeń nazw
namespace RafMat
{
namespace SimFile
{

using namespace std;

//
// Implementacja BlockInfo
//

BlockInfo::BlockInfo(string id, string name)
    : m_id(id), m_name(name)
{
}

string BlockInfo::operator [] (string assId) const throw (conf_writer_error)
{
    bool result;
    AssignmentMap::const_iterator elem = Utils::mapContains(m_assignments, assId, result);

    if(result)
        return elem->second;
    else
        throw conf_reader_error::unknownAssingment(*this, assId);
}

//
// Implementacja ConfigReader
//

const char _lbr_end[] = { ConfigReader::LBR_CHAR, ConfigReader::BLOCKEND_CHAR, 0 };
const std::string ConfigReader::LBR_END(_lbr_end);

ConfigReader::ConfigReader(istream& input)
    : m_currentLine(0),
      m_input(input)
{
}

BlockInfoSPtr ConfigReader::getConfiguration() throw (conf_reader_error)
{
    if(!m_evaluatedConfig)
        loadConfiguration();

    return m_evaluatedConfig;
}

BlockInfoSPtr ConfigReader::loadConfiguration() throw (conf_reader_error)
{
    BlockInfoSPtr currentBlock;

    // Do przechowywania jednej linii konfiguracji.
    string cfgLine;

    // Pętla po wszystkich liniach strumienia
    while(!m_input.eof())
    {
        ++m_currentLine;
        getline(m_input, cfgLine);

        // Usuwamy komentarze
        lexLine(cfgLine);

        // Czasami nic po tych operacjach nie zostaje - omijamy.
        if(cfgLine.empty())
            continue;

        // Parsujemy linię
        currentBlock = parseLine(cfgLine, currentBlock);
    }

    return currentBlock;
}

void ConfigReader::lexLine(std::string& line)
{
    int colonPos = line.find_first_of(COMMENT_CHAR);

    // Jeśli był komentarz to colonPos >= 0. Wtedy usuwamy komentarz.
    if(colonPos >= 0)
        line.resize(colonPos);

    // Usuwamy wiodące spacje
    line = Utils::trimWhiteSpaces(line);
}

BlockInfoSPtr ConfigReader::parseLine(const string& line, BlockInfoSPtr currentBlock) throw (conf_reader_error)
{
    // Spodziewamy się trzech rzeczy:
    // 1 - początku bloku
    // 2 - koniec bloki
    // 3 - przypisania wartości

    istringstream ostr(line);
    string startSymbol;
    string id1, id2;
    string endSymbol;

    ostr >> startSymbol;

    // Jeśli zaczyna się od "{/"
    if(startSymbol.length() == 1 && startSymbol[0] == LBR_CHAR)
    {
        ostr >> id1 >> id2 >> endSymbol;

        if(!Utils::matchesIdentifier(id1))
            throw conf_reader_error::syntaxError(m_currentLine, "'" + id1 + "' should be valid identifier.");

        if(!Utils::matchesIdentifier(id2))
            throw conf_reader_error::syntaxError(m_currentLine, "'" + id2 + "' should be valid identifier.");

        if(endSymbol.size() != 1 || endSymbol[0] != RBR_CHAR)
            throw conf_reader_error::syntaxError(m_currentLine, string("Expected block closing character '") + RBR_CHAR + "'.");

        BlockInfoSPtr nestedBlock(new BlockInfo(id1, id2));

        // Dopisujemy blok do drzewa i do stosu:
        // Jeśli drzewo jeszcze nie utworzone to traktujemy nestedBlock jak roota.
        if(currentBlock)
            currentBlock->m_children.push_back(nestedBlock);
        else if(m_evaluatedConfig)
            throw conf_reader_error::syntaxError(m_currentLine, "There must by only one root node.");
        else
            m_evaluatedConfig = nestedBlock;

        m_blockStack.push(nestedBlock);
        return nestedBlock;
    }
    // Jeśli zaczyna się od "{/"
    else if(startSymbol == LBR_END)
    {
        ostr >> id1 >> endSymbol;

        if(!Utils::matchesIdentifier(id1))
            throw conf_reader_error::syntaxError(m_currentLine, "'" + id1 + "' should be valid identifier.");

        if(endSymbol.size() != 1 || endSymbol[0] != RBR_CHAR)
            throw conf_reader_error::syntaxError(m_currentLine, string("Expected block closing character '") + RBR_CHAR + "'.");

        if(m_blockStack.empty())
            throw conf_reader_error::unbalansedStack(m_currentLine, id1, "none");

        BlockInfoSPtr parentBlk = m_blockStack.top();

        if(parentBlk->id() != id1)
            throw conf_reader_error::unbalansedStack(m_currentLine, id1, parentBlk->id());

        // Jeśli się powiodło, ściągamy ze stosu.
        m_blockStack.pop();

        // Zwracamy najwyższy element na stosie:
        return m_blockStack.empty() ? BlockInfoSPtr() : m_blockStack.top();
    }
    // Jeśli assignment
    else
    {
        ostr >> id1;

        if(!Utils::matchesIdentifier(startSymbol))
            throw conf_reader_error::syntaxError(m_currentLine, "'" + id1 + "' should be valid identifier.");

        if(id1 != "=")
            throw conf_reader_error::syntaxError(m_currentLine, "Expected '='. Notice spaces ' = ' are necessary.");

        // Wczytaj resztę jak kolwiek ona wygląda:
        getline(ostr, endSymbol);

        if(endSymbol.empty())
            throw conf_reader_error::syntaxError(m_currentLine, "Expected right side of assignment '='.");

        currentBlock->m_assignments[startSymbol] = Utils::trimWhiteSpaces(endSymbol);
        return currentBlock;
    }

    throw conf_reader_error::syntaxError(m_currentLine, "This line should be valid block start, block end or assignment.");
}

//
// Klasa wyjątków
//

conf_reader_error::conf_reader_error(const string& __arg)
    : logic_error("Error occured when reading configuration. " + __arg)
{
}

conf_reader_error conf_reader_error::syntaxError(int line, const string& why)
{
    ostringstream stream;

    stream << why;
    if(line > 0)
        stream << " Line " << line << ".";

    return conf_reader_error(stream.str());
}

conf_reader_error conf_reader_error::semanticError(const BlockInfo& blk, const std::string& why)
{
    ostringstream stream;

    stream << "At block '" << blk.id() << " " << blk.name() << "'. " << why;
    return conf_reader_error(stream.str());
}

conf_reader_error conf_reader_error::unbalansedStack(int line, const string& blockName, const string& expected)
{
    ostringstream stream;

    stream << "Closing block is not adequate to current open block. Expected '"
           << expected << "' but configuration reader found '" << blockName << "'.";

    if(line > 0)
        stream << " Line " << line << ".";

    return conf_reader_error(stream.str());
}

conf_reader_error conf_reader_error::unknownAssingment(const BlockInfo& malfunctBlock, const string& assId)
{
    ostringstream stream;

    using boost::lambda::_1;
    using boost::lambda::bind;
    typedef BlockInfo::AssignmentMap::value_type pair_type;

    stream << "User requests '" << assId << "' parameter which doesn't exist in block '"
           << malfunctBlock.id() << " " << malfunctBlock.name() << "'. "
           << "This block contains following parameters: ";

#if !defined(_MSC_VER)	// Pod MSVC jest jakiś problem z lambda-operatorem << nie chce mi sie szukać przyczyny.
    for_each(malfunctBlock.assignments().begin(), malfunctBlock.assignments().end(),
             stream << bind(&pair_type::first, _1) << "; ");
#endif

    return conf_reader_error(stream.str());
}

}
}
